﻿using UnityEngine;
using System;
using System.Collections;
using System.Threading;
using System.IO;
using System.Diagnostics;

using Debug = UnityEngine.Debug;

public class Region
{
    public override bool Equals(object obj)
    {
        Region r = obj as Region;
        if (obj == null)
            return false;

        return worldX.Equals(r.worldX) && worldZ.Equals(r.worldZ);
    }
    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 13;
            hash = hash * 3 + worldX.GetHashCode();
            hash = hash * 3 + worldZ.GetHashCode();
            return hash;
        }
    }

    public const short REGION_SIZE = VoxelTerrain.REGION_SIZE;
    public const byte REGION_HEIGHT = VoxelTerrain.WORLD_HEIGHT; // 128
    private const byte CHUNK_SIZE = VoxelTerrain.CHUNK_SIZE;
    public const int CHUNKS_PER_SIDE = REGION_SIZE / CHUNK_SIZE;
    public const int CHUNK_LAYER_AMOUNT = REGION_HEIGHT / CHUNK_SIZE;

    private const short KILOBYTE = 1 << 10;  // 1024
    private const int MEGABYTE = 1 << 20;  // 1048576

    private const byte F_LOC_SIZE = 4;
    private const byte F_CHUNK_SIZE = 4;
    public static Region FindOrCreate(Point3 point)
    {
        return FindOrCreate(point.x, point.z);
    }
    public static Region FindOrCreate(int x, int z)
    {
        Debug.Log("FindOrCreate called");
        if (File.Exists(string.Format("r.{0}.{1}.rgn", x / REGION_SIZE, z / REGION_SIZE)))
        {
            Region region = new Region(x / REGION_SIZE, z / REGION_SIZE);
            new Thread(region.FillRegionDataFromFile).Start();
            return region;
        }
        else
        {
            Region region = new Region(x / REGION_SIZE, z / REGION_SIZE);
            region.WriteRegionDataToFile();
            return region;
        }
    }
    public static Region Create(Point3 point)
    {
        return Create(point.x, point.z); 
    }
    public static Region Create(int x, int z)
    {
        if (File.Exists(string.Format("r.{0}.{1}.rgn", x / REGION_SIZE, z / REGION_SIZE)))
            File.Delete(string.Format("r.{0}.{1}.rgn", x / REGION_SIZE, z / REGION_SIZE));

        return FindOrCreate(x, z);
    }
    /// <summary>
    /// The x-coordinate of the Region in region space
    /// </summary>
    public int regionX { get; private set; } // 512
    /// <summary>
    /// The z-coordinate of the Region in region space
    /// </summary>       
    public int regionZ { get; private set; } // 512
    /// <summary>
    /// The position of the region in region space
    /// </summary>
    public Point2 regionPosition { get { return new Point2(regionX, regionZ); } }


    /// <summary>
    /// The x-coordinate of the Region in world space
    /// </summary>
    public int worldX { get { return regionX * REGION_SIZE; } }
    /// <summary>
    /// The z-coordinate of the Region in world space
    /// </summary>
    public int worldZ { get { return regionZ * REGION_SIZE; } }
    /// <summary>
    /// The position of the region in world space
    /// </summary>
    public Point2 worldPosition { get { return new Point2(worldX, worldZ); } }

    /// <summary>
    /// Container for the chunk objects
    /// </summary>
    private volatile Chunk[] chunkArray;

    private volatile bool isWriting;
    private volatile bool isReading;

    /// <summary>
    /// Creates a new region
    /// </summary>
    /// <param name="posX">The x-coordinate in region space</param>
    /// <param name="posZ">The z-coordinate in region space</param>
    private Region(int posX, int posZ)
    {
        regionX = posX;
        regionZ = posZ;

        chunkArray = new Chunk[CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT * CHUNKS_PER_SIDE];
        isWriting = false;
    }

    /// <summary>
    /// Creates a new chunk at given location in chunk coordinates
    /// </summary>
    /// <param name="x">The x-coordinate in chunk space</param>
    /// <param name="y">The y-coordinate in chunk space</param>
    /// <param name="z">The z-coordinate in chunk space</param>
    private void CreateChunkAt(byte x, byte y, byte z)
    {
        if (x < 0 || x >= CHUNKS_PER_SIDE || z < 0 || z >= CHUNKS_PER_SIDE || y < 0 || y >= CHUNK_LAYER_AMOUNT)
            throw new System.ArgumentException("Coordinates to create the chunks are out of range");

        chunkArray[x + y * CHUNKS_PER_SIDE + z * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT] = new Chunk(x * CHUNK_SIZE + regionX * REGION_SIZE,
                                                                                                                 y * CHUNK_SIZE,
                                                                                                                 z * CHUNK_SIZE + regionZ * REGION_SIZE);
    }
    /// <summary>
    /// Returns true if point is inside the chunk
    /// </summary>
    /// <param name="x">The x-coordinate in world space</param>
    /// <param name="z">The z-coordinate in world space</param>
    public bool IsInside(int x, int z)
    {
        return x >= worldX && z >= worldZ &&
               x < worldX + REGION_SIZE && z < worldZ + REGION_SIZE;
    }

    public static int counter = 0;

    public Chunk GetChunkAt(int x, int y, int z)
    {
        if (isReading)
            return new Chunk((x / CHUNK_SIZE) * CHUNK_SIZE, (y / CHUNK_SIZE) * CHUNK_SIZE, (z / CHUNK_SIZE) * CHUNK_SIZE);

        int ArrayX = (x % REGION_SIZE) / CHUNK_SIZE;
        int ArrayY = Mathf.Clamp(y, 0, VoxelTerrain.WORLD_HEIGHT) / CHUNK_SIZE;
        int ArrayZ = (z % REGION_SIZE) / CHUNK_SIZE;

        int index = ArrayX + ArrayY * CHUNKS_PER_SIDE + ArrayZ * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT;

        Chunk chunk = chunkArray[index];
        if (chunk != null)
        {
            return chunk;
        }
        else
        {
            chunkArray[index] = new Chunk((x / CHUNK_SIZE) * CHUNK_SIZE, (y / CHUNK_SIZE) * CHUNK_SIZE, (z / CHUNK_SIZE) * CHUNK_SIZE);
            //chunkArray[index].GenerateData();
            new Thread(chunkArray[index].GenerateData).Start();
            return chunkArray[index];
        }
    }
    public Chunk GetChunkAt(Point3 point)
    {
        return GetChunkAt(point.x, point.y, point.z);
    }

    /// <summary>
    /// Cleans and delete the chunk at given location in chunk space
    /// </summary>
    /// <param name="x">The x-coordinate in chunk space</param>
    /// <param name="y">The y-coordinate in chunk space</param>
    /// <param name="z">The z-coordinate in chunk space</param>
    private void DeleteChunkAt(byte x, byte y, byte z)
    {
        if (chunkArray[x + y * CHUNKS_PER_SIDE + z * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT] != null)
            chunkArray[x + y * CHUNKS_PER_SIDE + z * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT].CleanUp();

        chunkArray[x + y * CHUNKS_PER_SIDE + z * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT] = null;
    }

    //private byte ReadBlockFromFile(int posX, int posY, int posZ)
    //{
    //    int chunkPosX = posX / CHUNK_SIZE;
    //    int chunkPosY = posY / CHUNK_SIZE;
    //    int chunkPosZ = posZ / CHUNK_SIZE;

    //    int offsetX = posX & (CHUNK_SIZE - 1); // posX % CHUNK_SIZE (16)
    //    int offsetY = posY & (CHUNK_SIZE - 1); // posY % CHUNK_SIZE (16)
    //    int offsetZ = posZ & (CHUNK_SIZE - 1); // posZ % CHUNK_SIZE (16)

    //    using (BinaryReader reader = new BinaryReader(File.OpenRead("TestRegion.rgn")))
    //    {
    //        int pos = (chunkPosX + (chunkPosY * (REGION_SIZE / CHUNK_SIZE)) + (chunkPosZ * CHUNK_SIZE * CHUNK_SIZE)) * 4096; // Chunk Start Point
    //        pos += offsetX + (offsetY * CHUNK_SIZE) + (offsetZ * CHUNK_SIZE * CHUNK_SIZE); // Offset inside the chunk
    //        reader.BaseStream.Position = pos;

    //        return reader.ReadByte();
    //    }
    //}

    public void WriteRegionDataToFile()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        if (isWriting)
            return;
        Stopwatch GeneralTimer = new Stopwatch(); // WriteCommand = new Stopwatch(), PositionCommand = new Stopwatch();
        GeneralTimer.Start();

        isWriting = true;

        using (BinaryWriter writer = new BinaryWriter(File.Open(string.Format("r.{0}.{1}.rgn", regionX, regionZ), FileMode.Create, FileAccess.Write)))
        {
            int writerIndexStart;
            int location;
            int nextFreeSpace;

            // locations of the chunks (size: 4 KB)
            for (int ChunkPosX = 0; ChunkPosX < CHUNKS_PER_SIDE; ChunkPosX++)
                for (int ChunkPosY = 0; ChunkPosY < CHUNK_LAYER_AMOUNT; ChunkPosY++)
                    for (int ChunkPosZ = 0; ChunkPosZ < CHUNKS_PER_SIDE; ChunkPosZ++)
                    {
                        location = ChunkPosX + ChunkPosY * CHUNKS_PER_SIDE + ChunkPosZ * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT;
                        writerIndexStart = location * 4;
                        nextFreeSpace = location * 4096;

                        Chunk chunk = chunkArray[location];

                        writer.BaseStream.Position = writerIndexStart;

                        // Location
                        if (chunk == null)
                        {
                            writer.Write(0);
                            continue;
                        }

                        writer.Write(nextFreeSpace);

                        writer.BaseStream.Position = nextFreeSpace;

                        for (int i = 0; i < CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; i++)
                        {
                            writer.Write(chunk.chunkData[i]);
                        }
                    }
        }
        GeneralTimer.Stop();

        isWriting = false;
        stopwatch.Stop();
        Debug.Log("Writing took " + stopwatch.ElapsedMilliseconds + " ms");
        //Debug.Log(string.Format("General Time: {0}", GeneralTimer.ElapsedMilliseconds));
    }

    private void FillRegionDataFromFile()
    {
        if (isReading == true)
            return;

        isReading = true;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        chunkArray = new Chunk[CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT * CHUNKS_PER_SIDE];

        if (!File.Exists(string.Format("r.{0}.{1}.rgn", regionX, regionZ)))
        {
            return;
        }
        else
        {
            int location, readerStartIndex, nextFreeSpace;

                using (BinaryReader reader = new BinaryReader(File.OpenRead(string.Format("r.{0}.{1}.rgn", regionX, regionZ))))
                {
                    for (int ChunkPosX = 0; ChunkPosX < CHUNKS_PER_SIDE; ChunkPosX++)
                        for (int ChunkPosY = 0; ChunkPosY < CHUNK_LAYER_AMOUNT; ChunkPosY++)
                            for (int ChunkPosZ = 0; ChunkPosZ < CHUNKS_PER_SIDE; ChunkPosZ++)
                            {
                                location = ChunkPosX + ChunkPosY * CHUNKS_PER_SIDE + ChunkPosZ * CHUNKS_PER_SIDE * CHUNK_LAYER_AMOUNT;
                                readerStartIndex = location * 4;
                                nextFreeSpace = location * 4096;

                                reader.BaseStream.Position = readerStartIndex;
                                int locationIndex = reader.ReadInt32();
                                if (locationIndex == 0)
                                    continue;


                                Chunk chunk = chunkArray[location] = new Chunk(worldX + ChunkPosX * CHUNK_SIZE, ChunkPosY * CHUNK_SIZE, worldZ + ChunkPosZ * CHUNK_SIZE);
                                
                                reader.BaseStream.Position = nextFreeSpace;

                                for (int i = 0; i < CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; i++)
                                {
                                    chunk.chunkData[i] = reader.ReadByte(); // writer.Write(chunk.chunkData[i].GetValue());
                                }
                            }
                }
        }

        stopwatch.Stop();
        Debug.Log("Reading took " + stopwatch.ElapsedMilliseconds + " ms");

        isReading = false;
    }
}
